/**
 *
 */
package br.com.puertorico.model.querybuilder.handlers;

import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

import br.com.puertorico.model.querybuilder.wrappers.GraphElement;

/**
 * Esta classe existe para tratar dependencia cliclica
 *
 * @author daniel
 * @version 1.0.0
 */
@SuppressWarnings("unchecked")
public abstract class DependencyHandler<E extends GraphElement> {

	private DependencyGraph graph;

	/**
	 * @param elements
	 */
	DependencyHandler(List<E> elements) {
		graph = new DependencyGraph(elements, removeIfMissingDependency());
	}

	/**
	 * @param id
	 * @return os elementos requeridos associados ao id
	 */
	public Set<E> getRequiredElements(String id) {
		if (graph.isEmpty()) {
			return null;
		}
		Set<E> returnSet = new LinkedHashSet<E>();
		returnSet.add(graph.findNode(id).element);
		returnSet.addAll(graph.getRequiredElements(id));
		return returnSet;
	}

	/**
	 * @return os elementos requeridos
	 */
	public Set<E> getRequiredElements() {
		if (graph.isEmpty()) {
			return new LinkedHashSet<E>();
		}
		return graph.getRequiredElementsRemovingIfMissing();
	}

	/**
	 * @return true se deve remover os elementos caso os dependentes nao existam
	 */
	public abstract boolean removeIfMissingDependency();

	/**
	 * @return the graph
	 */
	public DependencyGraph getGraph() {
		return graph;
	}

	/**
	 * @param graph
	 *            the graph to set
	 */
	public void setGraph(DependencyGraph graph) {
		this.graph = graph;
	}

	/**
	 * @author daniel
	 * @version 1.0.0
	 * @param <E>
	 *            os elementos dos nodes
	 */
	private class DependencyGraph {

		private Set<Node> nodes;

		boolean isEmpty() {
			return nodes.isEmpty();
		}

		/**
		 * @param elements
		 * @param removeIfMissingDependency
		 */
		DependencyGraph(List<E> elements, boolean removeIfMissingDependency) {
			nodes = new LinkedHashSet<Node>();
			// inserir os nos
			for (E e : elements) {
				insertNode(e, e.getElementId());
			}
			// construir as arestas
			for (Node node : nodes) {
				for (String depe : node.getElement().getDependencies()) {
					if (depe == null || depe.equals("")) {
						continue;
					}
					Node to = findNode(depe);
					if (to != null) {
						node.insertEdge(to);
					} else if (removeIfMissingDependency) {
						node.setRemoved(true);
					}
				}
			}
		}

		/**
		 * Insere um novo noh no grafo
		 *
		 * @param element
		 * @param id
		 */
		public void insertNode(E element, String id) {
			nodes.add(new Node(id, element));
		}

		/**
		 * Procurar um elemento no grafo
		 *
		 * @param id
		 * @return
		 */
		public Node findNode(String id) {
			for (Node node : nodes) {
				if (node.getId().equals(id)) {
					return node;
				}
			}
			return null;
		}

		Set<E> getRequiredElementsRemovingIfMissing() {
			Set<E> returnSet = new LinkedHashSet<E>();
			for (Node node : nodes) {
				boolean valid = checkDependencyGraphRemovingIfMissing(node);
				if (valid) {
					returnSet.add(node.element);
				}
			}
			return returnSet;
		}

		/**
		 * @param node
		 * @return true se todas as dependencias para o node foram satisfeitas
		 */
		boolean checkDependencyGraphRemovingIfMissing(Node node) {
			// limpar o resultado de percuros anteriores
			for (Node n : nodes) {
				n.setVisited(false);
			}
			Queue<Node> fila = new LinkedList<Node>();
			if (node.isRemoved()) {
				return false;
			}
			fila.add(node);
			while (!fila.isEmpty()) {
				Node current = fila.poll();
				current.setVisited(true);
				for (Edge aresta : current.getEdges()) {
					if (!aresta.getTo().isVisited()) {
						if (aresta.getTo().isRemoved()) {
							return false;
						}
						fila.add(aresta.getTo());
					}
				}
			}
			return true;
		}

		/**
		 * @param id
		 * @return os dependentes do node com o id = id
		 */
		Set<E> getRequiredElements(String id) {
			// limpar o resultado de percuros anteriores
			for (Node n : nodes) {
				n.setVisited(false);
			}
			/*
			 * vamos montar a lista de dependentes usando busca por cobertura
			 * (BFS)
			 */
			Set<E> depes = new LinkedHashSet<E>();
			Queue<Node> fila = new LinkedList<Node>();
			fila.add(findNode(id));
			while (!fila.isEmpty()) {
				Node current = fila.poll();
				current.setVisited(true);
				/*
				 * adiciona na lista de retorno desconsiderando a noh que
				 * corresponde ao parametro
				 */
				if (!current.getId().equals(id)) {
					depes.add(current.getElement());
				}
				for (Edge aresta : current.getEdges()) {
					if (!aresta.getTo().isVisited()) {
						fila.add(aresta.getTo());
					}
				}
			}
			return depes;
		}

		/**
		 * @author daniel
		 * @version 1.0.0
		 */
		class Node {

			private String id;

			private E element;

			private Set<Edge> edges;

			private boolean removed;

			private boolean visited;

			/**
			 * Construtor principal
			 *
			 * @param id
			 * @param element
			 */
			Node(String id, E element) {
				setId(id);
				setElement(element);
			}

			void insertEdge(Node to) {
				getEdges().add(new Edge(to, this));
			}

			/**
			 * @return the element
			 */
			E getElement() {
				return element;
			}

			/**
			 * @param element
			 *            the element to set
			 */
			void setElement(E element) {
				this.element = element;
			}

			/**
			 * @return the id
			 */
			String getId() {
				return id;
			}

			/**
			 * @param id
			 *            the id to set
			 */
			void setId(String id) {
				this.id = id;
			}

			/**
			 * @return the edges
			 */
			Set<Edge> getEdges() {
				if (edges == null) {
					edges = new LinkedHashSet<Edge>();
				}
				return edges;
			}

			/**
			 * @return the removed
			 */
			boolean isRemoved() {
				return removed;
			}

			/**
			 * @param removed
			 *            the removed to set
			 */
			void setRemoved(boolean removed) {
				this.removed = removed;
			}

			/**
			 * @return the visited
			 */
			boolean isVisited() {
				return visited;
			}

			/**
			 * @param visited
			 *            the visited to set
			 */
			void setVisited(boolean visited) {
				this.visited = visited;
			}

			/*
			 * (non-Javadoc)
			 *
			 * @see java.lang.Object#equals(java.lang.Object)
			 */
			@Override
			public boolean equals(Object obj) {
				Node candidato = (Node) obj;
				return candidato.id.equals(id);
			}

			/*
			 * (non-Javadoc)
			 *
			 * @see java.lang.Object#hashCode()
			 */
			@Override
			public int hashCode() {
				return id.hashCode();
			}

		}

		/**
		 * @author daniel
		 * @version 1.0.0
		 */
		class Edge {

			private Node from;

			private Node to;

			/**
			 * @param to
			 * @param from
			 */
			Edge(Node to, Node from) {
				setTo(to);
				setFrom(from);
			}

			/**
			 * @param from
			 *            the from to set
			 */
			void setFrom(Node from) {
				this.from = from;
			}

			/**
			 * @return the to
			 */
			Node getTo() {
				return to;
			}

			/**
			 * @param to
			 *            the to to set
			 */
			void setTo(Node to) {
				this.to = to;
			}

			/*
			 * (non-Javadoc)
			 *
			 * @see java.lang.Object#equals(java.lang.Object)
			 */
			@Override
			public boolean equals(Object obj) {
				Edge candidato = (Edge) obj;
				return candidato.to.id.equals(to.id)
						&& candidato.from.id.equals(from.id);
			}

			/*
			 * (non-Javadoc)
			 *
			 * @see java.lang.Object#hashCode()
			 */
			@Override
			public int hashCode() {
				return to.id.hashCode() + from.id.hashCode();
			}
		}
	}

}
